import tensorflow as tf
import math
from tensorflow.examples.tutorials.mnist import input_data as mnist_data
print("Tensorflow version " + tf.__version__)
tf.set_random_seed(0)
import numpy as np
import cv2

mnist = mnist_data.read_data_sets("data", one_hot=True, reshape=False, validation_size=0)

# input X: 28x28 grayscale images, the first dimension (None) will index the images in the mini-batch
X = tf.placeholder(tf.float32, [None, 28, 28, 1])
# correct answers will go here
Y_ = tf.placeholder(tf.float32, [None, 10])
# variable learning rate
lr = tf.placeholder(tf.float32)
# test flag for batch norm
tst = tf.placeholder(tf.bool)
iter = tf.placeholder(tf.int32)
# dropout probability
pkeep = tf.placeholder(tf.float32)
pkeep_conv = tf.placeholder(tf.float32)

def batchnorm(Ylogits, is_test, iteration, offset, convolutional=False):
    exp_moving_avg = tf.train.ExponentialMovingAverage(0.999, iteration) # adding the iteration prevents from averaging across non-existing iterations
    bnepsilon = 1e-5
    if convolutional:
        mean, variance = tf.nn.moments(Ylogits, [0, 1, 2])
    else:
        mean, variance = tf.nn.moments(Ylogits, [0])
    update_moving_averages = exp_moving_avg.apply([mean, variance])
    m = tf.cond(is_test, lambda: exp_moving_avg.average(mean), lambda: mean)
    v = tf.cond(is_test, lambda: exp_moving_avg.average(variance), lambda: variance)
    Ybn = tf.nn.batch_normalization(Ylogits, m, v, offset, None, bnepsilon)
    return Ybn, update_moving_averages

def no_batchnorm(Ylogits, is_test, iteration, offset, convolutional=False):
    return Ylogits, tf.no_op()

def compatible_convolutional_noise_shape(Y):
    noiseshape = tf.shape(Y)
    noiseshape = noiseshape * tf.constant([1,0,0,1]) + tf.constant([0,1,1,0])
    return noiseshape

# three convolutional layers with their channel counts, and a
# fully connected layer (tha last layer has 10 softmax neurons)
K = 24  # first convolutional layer output depth
L = 48  # second convolutional layer output depth
M = 64  # third convolutional layer
N = 200  # fully connected layer

W1 = tf.Variable(tf.truncated_normal([5, 5, 1, K], stddev=0.1))  # 6x6 patch, 1 input channel, K output channels
B1 = tf.Variable(tf.constant(0.1, tf.float32, [K]))
W2 = tf.Variable(tf.truncated_normal([5, 5, K, L], stddev=0.1))
B2 = tf.Variable(tf.constant(0.1, tf.float32, [L]))
W3 = tf.Variable(tf.truncated_normal([4, 4, L, M], stddev=0.1))
B3 = tf.Variable(tf.constant(0.1, tf.float32, [M]))

W4 = tf.Variable(tf.truncated_normal([7 * 7 * M, N], stddev=0.1))
B4 = tf.Variable(tf.constant(0.1, tf.float32, [N]))
W5 = tf.Variable(tf.truncated_normal([N, 10], stddev=0.1))
B5 = tf.Variable(tf.constant(0.1, tf.float32, [10]))

# The model
# batch norm scaling is not useful with relus
# batch norm offsets are used instead of biases
stride = 1  # output is 28x28
Y1l = tf.nn.conv2d(X, W1, strides=[1, stride, stride, 1], padding='SAME')
Y1bn, update_ema1 = batchnorm(Y1l, tst, iter, B1, convolutional=True)
Y1r = tf.nn.relu(Y1bn)
Y1 = tf.nn.dropout(Y1r, pkeep_conv, compatible_convolutional_noise_shape(Y1r))
stride = 2  # output is 14x14
Y2l = tf.nn.conv2d(Y1, W2, strides=[1, stride, stride, 1], padding='SAME')
Y2bn, update_ema2 = batchnorm(Y2l, tst, iter, B2, convolutional=True)
Y2r = tf.nn.relu(Y2bn)
Y2 = tf.nn.dropout(Y2r, pkeep_conv, compatible_convolutional_noise_shape(Y2r))
stride = 2  # output is 7x7
Y3l = tf.nn.conv2d(Y2, W3, strides=[1, stride, stride, 1], padding='SAME')
Y3bn, update_ema3 = batchnorm(Y3l, tst, iter, B3, convolutional=True)
Y3r = tf.nn.relu(Y3bn)
Y3 = tf.nn.dropout(Y3r, pkeep_conv, compatible_convolutional_noise_shape(Y3r))

# reshape the output from the third convolution for the fully connected layer
YY = tf.reshape(Y3, shape=[-1, 7 * 7 * M])

Y4l = tf.matmul(YY, W4)
Y4bn, update_ema4 = batchnorm(Y4l, tst, iter, B4)
Y4r = tf.nn.relu(Y4bn)
Y4 = tf.nn.dropout(Y4r, pkeep)
Ylogits = tf.matmul(Y4, W5) + B5
Y = tf.nn.softmax(Ylogits)

update_ema = tf.group(update_ema1, update_ema2, update_ema3, update_ema4)

# cross-entropy loss function (= -sum(Y_i * log(Yi)) ), normalised for batches of 100  images
# TensorFlow provides the softmax_cross_entropy_with_logits function to avoid numerical stability
# problems with log(0) which is NaN
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=Ylogits, labels=Y_)
cross_entropy = tf.reduce_mean(cross_entropy)*100

# accuracy of the trained model, between 0 (worst) and 1 (best)
correct_prediction = tf.equal(tf.argmax(Y, 1), tf.argmax(Y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

# training step, the learning rate is a placeholder
train_step = tf.train.AdamOptimizer(lr).minimize(cross_entropy)

init = tf.global_variables_initializer()
# save model
Saver = tf.train.Saver(max_to_keep = 1)  # defaults to saving all variables
ModelSavePath = './storage_bn/'
def train():
	with tf.Session() as sess:
		sess.run(init)
		ckpt = tf.train.get_checkpoint_state(ModelSavePath)
		if ckpt and ckpt.model_checkpoint_path:
			Saver.restore(sess, ckpt.model_checkpoint_path)
			print '----> restore model ok'
		else:
			print '----> no model so far'
		iterNum = 20000
		for i in xrange(iterNum):
			if (i + 1) % 10 == 0:
				Saver.save(sess, ModelSavePath + "model", global_step = (i + 1))
			# training on batches of 100 images with 100 labels
			batch_X, batch_Y = mnist.train.next_batch(100)
			# learning rate decay
			max_learning_rate = 0.003
			min_learning_rate = 0.0001
			decay_speed = 1600.0 # 0.003-0.0001-2000=>0.9826 done in 5000 iterations
			learning_rate = min_learning_rate + (max_learning_rate - min_learning_rate) * math.exp(-i/decay_speed)
			# the backpropagation training step
			a, c, s = sess.run([accuracy, cross_entropy, train_step], \
					  {X: batch_X, Y_: batch_Y, lr: learning_rate, tst: False, pkeep: 0.75, pkeep_conv: 1.0})
			sess.run(update_ema, {X: batch_X, Y_: batch_Y, tst: False, iter: i, pkeep: 1.0, pkeep_conv: 1.0})
			if (i + 1) % 10 == 0:
				print iterNum, 	'%6d'%i, \
								'%9.3f(cross entropy) '%c, \
								'%9.3f(training accuracy) '%a, \
								'%9.5f(learning rate) '%learning_rate

def test():
	batch_X = np.zeros([6, 28, 28, 1])
	batch_X[0] = np.expand_dims(cv2.imread('../z0.jpg', cv2.IMREAD_GRAYSCALE).astype('float32')/ 255.0, -1)
	batch_X[1] = np.expand_dims(cv2.imread('../z1.jpg', cv2.IMREAD_GRAYSCALE).astype('float32')/ 255.0, -1)
	batch_X[2] = np.expand_dims(cv2.imread('../z2.jpg', cv2.IMREAD_GRAYSCALE).astype('float32')/ 255.0, -1)
	batch_X[3] = np.expand_dims(cv2.imread('../z3.jpg', cv2.IMREAD_GRAYSCALE).astype('float32')/ 255.0, -1)
	batch_X[4] = np.expand_dims(cv2.imread('../z4.jpg', cv2.IMREAD_GRAYSCALE).astype('float32')/ 255.0, -1)
	batch_X[5] = np.expand_dims(cv2.imread('../z5.jpg', cv2.IMREAD_GRAYSCALE).astype('float32')/ 255.0, -1)
	trueY = np.array([6, 7, 2, 4, 3, 5])

	with tf.Session() as sess:
		sess.run(init)
		ckpt = tf.train.get_checkpoint_state(ModelSavePath)
		if ckpt and ckpt.model_checkpoint_path:
			Saver.restore(sess, ckpt.model_checkpoint_path)
			print '----> restore model ok'
		else:
			print '----> no model so far'
		oy = sess.run(Y, {X: batch_X, tst: True, pkeep: 1.0, pkeep_conv: 1.0})
		for i in range(0, 6):
			for j in range(0, 10):
				if(oy[i, j] > 0.6):
					print i, trueY[i], j
					break

def test1():
	batch_X, batch_Y = mnist.train.next_batch(1)
	img = batch_X[0,:,:,0] * 255.0
	img = img.astype('uint8')
	print img
#	cv2.imshow('win', img)
#	cv2.waitKey(0)
	print batch_Y
	
	with tf.Session() as sess:
		sess.run(init)
		ckpt = tf.train.get_checkpoint_state(ModelSavePath)
		if ckpt and ckpt.model_checkpoint_path:
			Saver.restore(sess, ckpt.model_checkpoint_path)
			print '----> restore model ok'
		else:
			print '----> no model so far'
		oy = sess.run(Y, {X: batch_X, pkeep: 1.0})
		print oy

if __name__ == "__main__":
#	train()
	test()

